# calibration_window.py (分离拟合与保存，调整为左右布局)
import copy
import json
import os

from PyQt6.QtGui import QPainter, QPageLayout
from PyQt6.QtPrintSupport import QPrinter
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QGroupBox, QPushButton, QComboBox,
                             QGridLayout, QLabel, QDoubleSpinBox, QTableWidget, QTableWidgetItem,
                             QHeaderView, QRadioButton, QMessageBox, QInputDialog, QFileDialog,
                             QSpinBox, QHBoxLayout, QCheckBox, QPlainTextEdit, QStyledItemDelegate)
from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QDateTime
import pyqtgraph as pg
from collections import deque, defaultdict
import numpy as np
import pyqtgraph.exporters

# 在 calibration_window.py 顶部
from style import PRIMARY_BUTTON_STYLE, SUCCESS_BUTTON_STYLE, DANGER_BUTTON_STYLE, WARNING_BUTTON_STYLE, INFO_BUTTON_STYLE
from table_model import CalibrationDataModel, ChannelDelegate


# =============================================================================
#  Custom Delegate for the Channel Column (Unchanged)
# =============================================================================


class CalibrationWindow(QWidget):
    request_pd_voltage_reading = pyqtSignal()
    window_closed = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlags(self.windowFlags() | Qt.WindowType.Window)
        self.setWindowTitle("通道标定")
        self.setGeometry(200, 200, 1000, 700)
        self.channel_colors = ['b', 'r', 'g', 'c', 'm', 'y', 'k']
        self.latest_filtered_voltages = [0.0] * 12
        self.is_waiting_for_sample = False
        self.pending_calibration_info = {}
        self.plot_checkboxes = []
        self.channel_delegate = ChannelDelegate(self)

        # <--- MODIFIED: Create separate storage for each mode ---
        # Single-channel mode storage
        self.single_channel_data_model = CalibrationDataModel()
        self.single_channel_reference_point = None
        self.single_channel_fits = {}

        # 12-channel mode storage
        self.multi_channel_data_model = CalibrationDataModel()
        self.multi_channel_reference_point = None
        self.multi_channel_fits = {}

        # General-purpose pointers that will switch between the above stores
        self.data_model = self.single_channel_data_model  # Start in single-channel mode
        self.reference_point = self.single_channel_reference_point
        self.fit_polynomials = self.single_channel_fits

        self._sort_column = 0
        self._sort_order = Qt.SortOrder.AscendingOrder

        self.sample_timeout_timer = QTimer(self)
        self.sample_timeout_timer.setSingleShot(True)
        self.sample_timeout_timer.timeout.connect(self._on_sample_timeout)

        self._init_ui()
        self._on_mode_changed()  # Call initial setup
        self.on_connection_status_changed(False)
        # self._initialize_with_default_data()

    def _init_ui(self):
        # ... (UI layout code is mostly unchanged)
        main_layout = QHBoxLayout(self)
        left_panel = QWidget();
        left_layout = QVBoxLayout(left_panel);
        left_panel.setFixedWidth(450)
        right_panel = QWidget();
        right_layout = QVBoxLayout(right_panel)
        main_layout.addWidget(left_panel);
        main_layout.addWidget(right_panel, 1)
        mode_groupbox = QGroupBox("选择标定模式");
        mode_layout = QHBoxLayout();
        mode_groupbox.setLayout(mode_layout)
        self.radio_single = QRadioButton("单路标定");
        self.radio_12ch = QRadioButton("12路同时标定")
        mode_layout.addWidget(self.radio_single);
        mode_layout.addWidget(self.radio_12ch);
        self.radio_single.setChecked(True)
        left_layout.addWidget(mode_groupbox)
        self.control_groupbox = QGroupBox("数据采集与处理");
        ops_layout = QGridLayout();
        self.control_groupbox.setLayout(ops_layout)
        self.label_channel = QLabel("选择通道:");
        self.channel_combobox = QComboBox();
        self.channel_combobox.addItems([f"通道 {i + 1}" for i in range(12)])
        ops_layout.addWidget(self.label_channel, 0, 0);
        ops_layout.addWidget(self.channel_combobox, 0, 1)
        ops_layout.addWidget(QLabel("输入温度 (°C):"), 1, 0);
        self.temp_spinbox = QDoubleSpinBox();
        self.temp_spinbox.setRange(-50.0, 200.0);
        self.temp_spinbox.setDecimals(2);
        self.temp_spinbox.setValue(0.0)
        ops_layout.addWidget(self.temp_spinbox, 1, 1)
        sample_layout = QHBoxLayout();
        self.sample_button = QPushButton("记录当前点");
        # self.sample_button.setStyleSheet(WARNING_BUTTON_STYLE)
        self.sample_button.clicked.connect(self._on_sample_clicked)
        self.cancel_button = QPushButton("取消");
        self.cancel_button.clicked.connect(self._on_cancel_clicked);
        self.cancel_button.setVisible(False)
        sample_layout.addWidget(self.sample_button);
        sample_layout.addWidget(self.cancel_button)
        ops_layout.addLayout(sample_layout, 2, 0, 1, 4)
        table_edit_layout = QHBoxLayout()
        add_button = QPushButton("添加新行", clicked=self._on_add_row)
        add_button.setStyleSheet(PRIMARY_BUTTON_STYLE)
        table_edit_layout.addWidget(add_button,1)
        delete_button = QPushButton("删除选中行", clicked=self._on_delete_row)
        delete_button.setStyleSheet(DANGER_BUTTON_STYLE)
        table_edit_layout.addWidget(delete_button,1)
        self.save_table_button = QPushButton("保存数据")
        self.save_table_button.clicked.connect(self._on_save_table_data)
        self.save_table_button.setStyleSheet(SUCCESS_BUTTON_STYLE)
        table_edit_layout.addWidget(self.save_table_button,1)
        self.restore_data_button = QPushButton("读取数据")
        self.restore_data_button.clicked.connect(self._on_restore_data)
        self.restore_data_button.setStyleSheet(INFO_BUTTON_STYLE)
        table_edit_layout.addWidget(self.restore_data_button,1)
        ops_layout.addLayout(table_edit_layout, 3, 0, 1, 4)

        self.table_widget = QTableWidget()
        self.table_widget.itemChanged.connect(self._on_table_item_changed)
        self.table_widget.setSortingEnabled(False)
        self.table_widget.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
        self.table_widget.horizontalHeader().sectionClicked.connect(self._on_header_clicked)

        ops_layout.addWidget(self.table_widget, 4, 0, 1, 4)
        self.ref_button = QPushButton("设为基准点", clicked=self._on_select_reference);
        ops_layout.addWidget(self.ref_button, 0, 2, 1, 2)
        self.ref_label = QLabel("基准点: 未设置");
        self.ref_label.setAlignment(Qt.AlignmentFlag.AlignCenter);
        ops_layout.addWidget(self.ref_label, 1, 2, 1, 2)
        left_layout.addWidget(self.control_groupbox, 1)
        fit_groupbox = QGroupBox("执行拟合与保存");
        fit_layout = QHBoxLayout();
        fit_groupbox.setLayout(fit_layout);
        fit_layout.addWidget(QLabel("多项式阶数:"))
        self.poly_degree_spinbox = QSpinBox();
        self.poly_degree_spinbox.setRange(1, 10);
        self.poly_degree_spinbox.setValue(2);
        fit_layout.addWidget(self.poly_degree_spinbox)
        self.fit_button = QPushButton("执行拟合");
        self.fit_button.clicked.connect(self._on_perform_fit);
        fit_layout.addWidget(self.fit_button);
        fit_layout.addStretch()
        self.save_image_button = QPushButton("保存图像");
        self.save_image_button.clicked.connect(self._on_save_plot_image);
        fit_layout.addWidget(self.save_image_button)
        self.save_fit_button = QPushButton("保存结果");
        self.save_fit_button.clicked.connect(self._on_save_results);
        self.save_fit_button.setEnabled(False);
        fit_layout.addWidget(self.save_fit_button)
        left_layout.addWidget(fit_groupbox)
        plot_select_groupbox = QGroupBox("绘图显示选择");
        plot_select_layout = QGridLayout();
        plot_select_groupbox.setLayout(plot_select_layout)
        self.plot_checkboxes = []
        for i in range(12):
            checkbox = QCheckBox(f"CH{i + 1}");
            checkbox.stateChanged.connect(self._update_visualization);
            self.plot_checkboxes.append(checkbox);
            plot_select_layout.addWidget(checkbox, i // 4, i % 4)
        right_layout.addWidget(plot_select_groupbox)
        self.fit_equation_display = QPlainTextEdit();
        self.fit_equation_display.setReadOnly(True);
        self.fit_equation_display.setPlainText("拟合结果将显示在此处");
        self.fit_equation_display.setFixedHeight(120);
        self.fit_equation_display.setStyleSheet("font-family: Courier New;");
        right_layout.addWidget(self.fit_equation_display)
        self.plot_widget = pg.PlotWidget();
        self.plot_widget.setBackground('w');
        self.plot_widget.showGrid(x=True, y=True);
        self.plot_widget.setLabel('left', '电压/基准电压');
        self.plot_widget.setLabel('bottom', '温度 (°C)');
        self.plot_widget.addLegend()
        right_layout.addWidget(self.plot_widget, 1)
        self.radio_single.toggled.connect(self._on_mode_changed)

    # ... (Methods like _on_restore_data, etc. are unchanged)
    def _on_restore_data(self):
        filename, _ = QFileDialog.getOpenFileName(self, "恢复标定数据", "", "JSON Files (*.json)")
        if not filename: return
        try:
            with open(filename, 'r', encoding='utf-8') as f:
                data = json.load(f)
            self._load_data_from_dict(data)
        except Exception as e:
            QMessageBox.critical(self, "文件错误", f"无法读取或解析文件:\n{e}")
        self._sort_and_refresh_table()  # Refresh after loading

    def _load_data_from_dict(self, data: dict):
        if "data_points" in data:
            self._load_raw_data_points(data)
        elif "fit_results" in data:
            self._load_fit_results(data)
        else:
            QMessageBox.warning(self, "格式无效", "无法识别的JSON文件格式。"); return
        QMessageBox.information(self, "成功", "数据已成功恢复。")

    def _load_raw_data_points(self, data: dict):
        mode = data.get("mode", "single-channel")
        self.radio_single.blockSignals(True)
        self.radio_12ch.blockSignals(True)
        if mode == "single-channel":
            self.radio_single.setChecked(True)
        else:
            self.radio_12ch.setChecked(True)
        self.radio_single.blockSignals(False)
        self.radio_12ch.blockSignals(False)
        self._on_mode_changed()

        points = data.get("data_points", [])
        if mode == "single-channel":
            for point in points:
                if 'channel' in point:
                    point['channel'] -= 1

        for point in points:
            self.data_model.add_point(point)  # Add directly to model

    def _load_fit_results(self, data: dict):
        mode = data.get("mode", "single-channel")
        self.radio_single.blockSignals(True);
        self.radio_12ch.blockSignals(True)
        if mode == "single-channel":
            self.radio_single.setChecked(True)
        else:
            self.radio_12ch.setChecked(True)
        self.radio_single.blockSignals(False);
        self.radio_12ch.blockSignals(False)
        self._on_mode_changed()
        cal_data = data.get("calibration_data(unit:C)", {});
        reconstructed_points = []
        if mode == "single-channel":
            for temp_str, ch_volt_map in cal_data.items():
                temp = float(temp_str)
                for ch_str, volt in ch_volt_map.items():
                    point = {'temp': temp, 'channel': int(ch_str) - 1, 'voltage': volt};
                    reconstructed_points.append(point)
        else:
            for temp_str, ch_volt_map in cal_data.items():
                temp = float(temp_str);
                voltages = [0.0] * 12
                for ch_str, volt in ch_volt_map.items():
                    ch_index = int(ch_str) - 1
                    if 0 <= ch_index < 12: voltages[ch_index] = volt
                point = {'temp': temp, 'voltages': voltages};
                reconstructed_points.append(point)
        for point in reconstructed_points:
            self.data_model.add_point(point)
        ref_temp = data.get("reference_temperature")
        if ref_temp is not None:
            for i, point in enumerate(self.data_model.data):
                if np.isclose(point['temp'], ref_temp):
                    self.reference_point = point;
                    self.ref_label.setText(
                        f"基准点: CH{point.get('channel', 0) + 1}, T={self.reference_point['temp']:.2f}°C" if self.radio_single.isChecked() else f"基准点: T={self.reference_point['temp']:.2f}°C");
                    break

    # <--- CHANGE 3: The old _on_sort_changed is DELETED ---

    # <--- CHANGE 4: This NEW method handles header clicks ---
    def _on_header_clicked(self, logicalIndex):
        """Manually handles sorting when a header is clicked."""
        # If the same column is clicked, reverse the sort order
        if logicalIndex == self._sort_column:
            self._sort_order = Qt.SortOrder.DescendingOrder if self._sort_order == Qt.SortOrder.AscendingOrder else Qt.SortOrder.AscendingOrder
        # If a new column is clicked, set it as the sort column and default to ascending
        else:
            self._sort_column = logicalIndex
            self._sort_order = Qt.SortOrder.AscendingOrder

        self._sort_and_refresh_table()

    # <--- CHANGE 5: This NEW method contains the sorting and refreshing logic ---
    def _sort_and_refresh_table(self):
        """Sorts the data model and refreshes the table view from it."""
        is_single = self.radio_single.isChecked()
        sort_key = None

        # Determine the key to sort the data model by
        if self._sort_column == 0:
            sort_key = 'temp'
        elif is_single and self._sort_column == 1:
            sort_key = 'channel'
        elif is_single and self._sort_column == 2:
            sort_key = 'voltage'
        elif not is_single and self._sort_column > 0:
            ch_index = self._sort_column - 1
            sort_key = lambda p: p['voltages'][ch_index]

        if sort_key:
            # Sort the actual data in the model
            self.data_model.data.sort(
                key=sort_key if callable(sort_key) else lambda p: p.get(sort_key, 0),
                reverse=(self._sort_order == Qt.SortOrder.DescendingOrder)
            )

        # Refresh the entire table from the now-sorted model
        self._refresh_table_from_model()
        # Manually set the visual sort indicator on the header
        self.table_widget.horizontalHeader().setSortIndicator(self._sort_column, self._sort_order)

    def _initialize_with_default_data(self):
        self.data_model.clear()  # Clear previous data
        is_single_mode = self.radio_single.isChecked()
        if is_single_mode:
            default_points = [{'temp': 25.0, 'voltage': 2.0, 'channel': 0}, {'temp': 10.0, 'voltage': 1.1, 'channel': 0}, {'temp': 15.0, 'voltage': 1.5, 'channel': 0}]
            for point in default_points: self.data_model.add_point(point)
        else:
            default_points_12ch = [{'temp': 10.0, 'voltages': [1.50, 1.80, 5.20, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]},
                                   {'temp': 25.0, 'voltages': [2.25, 1.50, 5.88, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]},
                                   {'temp': 50.0, 'voltages': [3.50, 1.00, 8.00, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]}]
            for point in default_points_12ch: self.data_model.add_point(point)

        self._sort_and_refresh_table()
        print(f"已加载 {'单路' if is_single_mode else '12路'} 模式的默认测试数据。")

    def _reset_calibration(self):
        self.table_widget.setRowCount(0)
        self.data_model.clear()
        self.reference_point = None
        self.fit_polynomials.clear()
        self.ref_label.setText("基准点: 未设置")
        self.save_fit_button.setEnabled(False)
        for checkbox in self.plot_checkboxes: checkbox.setChecked(False); checkbox.setEnabled(False)
        self.fit_equation_display.setPlainText("拟合结果：")
        self._update_visualization()
        # Reset sort indicator
        self.table_widget.horizontalHeader().setSortIndicator(-1, Qt.SortOrder.AscendingOrder)

    def _refresh_table_from_model(self):
        self.table_widget.blockSignals(True)
        self.table_widget.setRowCount(0)
        for point in self.data_model.data:
            self._add_point_to_table(point)
        self.table_widget.blockSignals(False)

    def _add_point_to_table(self, point_data: dict):
        # This function no longer needs to block signals, _refresh_table_from_model does it.
        row = self.table_widget.rowCount()
        self.table_widget.insertRow(row)
        temp = point_data.get('temp', 0.0)
        temp_item = QTableWidgetItem()
        temp_item.setData(Qt.ItemDataRole.EditRole, temp)
        self.table_widget.setItem(row, 0, temp_item)

        if self.radio_single.isChecked():
            channel_index = point_data.get('channel', 0)
            voltage = point_data.get('voltage', 0.0)
            ch_item = QTableWidgetItem(f"通道 {channel_index + 1}")
            self.table_widget.setItem(row, 1, ch_item)
            volt_item = QTableWidgetItem()
            volt_item.setData(Qt.ItemDataRole.EditRole, voltage)
            self.table_widget.setItem(row, 2, volt_item)
        else:
            voltages = point_data.get('voltages', [0.0] * 12)
            for i in range(12):
                volt_item = QTableWidgetItem()
                volt_item.setData(Qt.ItemDataRole.EditRole, voltages[i])
                self.table_widget.setItem(row, i + 1, volt_item)

        self.table_widget.scrollToBottom()

    def _on_mode_changed(self):
        new_mode_is_single = self.radio_single.isChecked()
        # <--- FIX: This check is now robust for initial startup ---
        # It checks the actual UI state (column count) to determine the old mode.
        # On startup, columnCount is 0, so old_mode_is_single will be False,
        # ensuring the initial setup for single-mode runs correctly.
        old_mode_is_single = (self.table_widget.columnCount() == 3)

        if new_mode_is_single == old_mode_is_single:
            return  # No change, do nothing

        # --- 1. Save the state of the mode we are leaving ---
        if old_mode_is_single:
            self.single_channel_reference_point = self.reference_point
            self.single_channel_fits = self.fit_polynomials
        else:
            self.multi_channel_reference_point = self.reference_point
            self.multi_channel_fits = self.fit_polynomials

        # --- 2. Clear UI and switch to the new mode's state ---
        self.table_widget.setRowCount(0)
        self.fit_equation_display.setPlainText("拟合结果将显示在此处")
        self.plot_widget.clear()

        if new_mode_is_single:
            # Load single-channel context
            self.data_model = self.single_channel_data_model
            self.reference_point = self.single_channel_reference_point
            self.fit_polynomials = self.single_channel_fits
        else:
            # Load 12-channel context
            self.data_model = self.multi_channel_data_model
            self.reference_point = self.multi_channel_reference_point
            self.fit_polynomials = self.multi_channel_fits

        # --- 3. Reconfigure the UI for the new mode ---
        self.channel_combobox.setEnabled(new_mode_is_single)
        self.label_channel.setEnabled(new_mode_is_single)

        if new_mode_is_single:
            self.table_widget.setColumnCount(3)
            self.table_widget.setHorizontalHeaderLabels(["温度 (°C)", "通道", "电压 (V)"])
            self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
            self.table_widget.setItemDelegateForColumn(1, self.channel_delegate)
        else:
            self.table_widget.setColumnCount(13)
            headers = ["温度 (°C)"] + [f"CH{i + 1} (V)" for i in range(12)]
            self.table_widget.setHorizontalHeaderLabels(headers)
            self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
            self.table_widget.setItemDelegateForColumn(1, None)

        # --- 4. Refresh the UI with the loaded state ---
        # This will populate the table if there's any saved data for this mode.
        self._update_ui_from_restored_state()
    def _update_ui_from_restored_state(self):
        """Refreshes the entire UI based on the currently loaded context."""
        # 1. Update reference point label
        if self.reference_point:
            base_text = f"T={self.reference_point['temp']:.2f}°C"
            if self.radio_single.isChecked():
                channel_index = self.reference_point.get('channel', 0);
                label_text = f"基准点: CH{channel_index + 1}, {base_text}"
            else:
                label_text = f"基准点: {base_text}"
            self.ref_label.setText(label_text)
        else:
            self.ref_label.setText("基准点: 未设置")

        # 2. Update equation display and button state
        self._update_equation_display()
        self.save_fit_button.setEnabled(bool(self.fit_polynomials))

        # 3. Repopulate and sort the table
        self._sort_and_refresh_table()

        # 4. Update plot checkboxes based on loaded fits
        for i, checkbox in enumerate(self.plot_checkboxes):
            if i in self.fit_polynomials:
                checkbox.setEnabled(True)
                checkbox.setChecked(True)  # Or just set enabled, your choice
            else:
                checkbox.setEnabled(False)
                checkbox.setChecked(False)

        # 5. Redraw the plot
        self._update_visualization()


    def _on_save_table_data(self):
        if not self.data_model.data:
            QMessageBox.warning(self, "无数据", "表格中没有数据点可以保存。")
            return
        current_time = QDateTime.currentDateTime().toString("yyyy-MM-dd_hh-mm-ss")
        default_filename = f"calibration_points_{current_time}.json"
        filename, _ = QFileDialog.getSaveFileName(self, "保存数据点", default_filename, "JSON Files (*.json)")

        if not filename:
            return

        is_single = self.radio_single.isChecked()
        data_to_save = copy.deepcopy(self.data_model.data)
        # 如果是单通道模式，将所有 channel 字段的值 +1
        if is_single:
            for point in data_to_save:
                if 'channel' in point:
                    point['channel'] += 1
        output_data = {"mode": "single-channel" if is_single else "12-channel", "data_points": data_to_save}
        try:
            with open(filename, 'w') as f:
                json.dump(output_data, f, indent=4)  # indent=4 更易读
            QMessageBox.information(self, "成功", f"数据点已保存到:\n{filename}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存文件失败: {e}")

    def _on_perform_fit(self):
        degree = self.poly_degree_spinbox.value();
        if not self.reference_point: QMessageBox.warning(self, "警告", "请先选择基准点。"); return
        self.fit_polynomials.clear()
        for checkbox in self.plot_checkboxes: checkbox.setEnabled(False)
        fit_successful = False
        if self.radio_single.isChecked():
            fit_successful = self._fit_single_channel()
        else:
            fit_successful = self._fit_12_channel()
        if fit_successful:
            self.save_fit_button.setEnabled(True);
            self._update_equation_display();
            self._update_visualization();
            QMessageBox.information(self, "成功", "数据拟合完成，现在可以保存拟合结果。")

    def _update_equation_display(self):
        if not self.fit_polynomials: self.fit_equation_display.setPlainText("拟合结果："); return
        full_text = []
        if self.radio_12ch.isChecked():
            full_text.append("--- 12通道拟合结果 ---")
            for i in range(12):
                if i in self.fit_polynomials: full_text.append(f"CH{i + 1}: y = {self._format_poly_string(self.fit_polynomials[i])}")
        else:
            ch_index = self.channel_combobox.currentIndex()
            if ch_index in self.fit_polynomials:
                poly_str = self._format_poly_string(self.fit_polynomials[ch_index])
                full_text.append(f"通道 {ch_index + 1} 拟合曲线:");
                full_text.append(f"y = {poly_str}")
        self.fit_equation_display.setPlainText("\n".join(full_text))

    def _format_poly_string(self, poly):
        terms = []
        for i, c in enumerate(poly.coeffs):
            power = poly.order - i
            if abs(c) < 1e-9: continue
            if power > 1:
                terms.append(f"{c:+.4g} T^{power}")
            elif power == 1:
                terms.append(f"{c:+.4g} T")
            else:
                terms.append(f"{c:+.4g}")
        if not terms: return "0.0"
        return " ".join(terms).lstrip('+ ')

    def _fit_single_channel(self):
        ch_index = self.channel_combobox.currentIndex();
        x_temps, y_volts = self.data_model.get_data_for_fit(True, ch_index)
        if x_temps is None: QMessageBox.warning(self, "数据不足", f"通道 {ch_index + 1} 没有标定数据点。"); return False
        y0_ref = self.data_model.get_reference_voltage(self.reference_point, True, ch_index)
        if y0_ref is None: QMessageBox.warning(self, "基准点错误", f"请为通道 {ch_index + 1} 选择一个对应的基准点。"); return False
        x_temps, y_volts = np.array(x_temps), np.array(y_volts);
        y_normalized = y_volts / y0_ref
        coeffs = np.polyfit(x_temps, y_normalized, self.poly_degree_spinbox.value());
        self.fit_polynomials[ch_index] = np.poly1d(coeffs)
        self.plot_checkboxes[ch_index].setEnabled(True);
        self.plot_checkboxes[ch_index].blockSignals(True);
        self.plot_checkboxes[ch_index].setChecked(True);
        self.plot_checkboxes[ch_index].blockSignals(False)
        return True

    def _fit_12_channel(self):
        for i in range(12):
            x_temps, y_volts = self.data_model.get_data_for_fit(False, i);
            y0_ref = self.data_model.get_reference_voltage(self.reference_point, False, i)
            if x_temps is None or y0_ref is None: continue
            x_temps, y_volts = np.array(x_temps), np.array(y_volts);
            y_normalized = y_volts / y0_ref if y0_ref != 0 else y_volts
            coeffs = np.polyfit(x_temps, y_normalized, self.poly_degree_spinbox.value());
            self.fit_polynomials[i] = np.poly1d(coeffs)
        for checkbox in self.plot_checkboxes: checkbox.setEnabled(True); checkbox.blockSignals(True); checkbox.setChecked(True); checkbox.blockSignals(False)
        self._update_visualization();
        return True

    def _update_visualization(self):
        self.plot_widget.clear();
        self.plot_widget.addLegend()
        if not self.data_model or not self.reference_point: return
        for i, checkbox in enumerate(self.plot_checkboxes):
            if checkbox.isChecked() and i in self.fit_polynomials:
                poly = self.fit_polynomials[i];
                color = self.channel_colors[i % len(self.channel_colors)]
                x_temps, y_volts = self.data_model.get_data_for_fit(self.radio_single.isChecked(), i);
                y0_ref = self.data_model.get_reference_voltage(self.reference_point, self.radio_single.isChecked(), i)
                if x_temps is None or y0_ref is None: continue
                x_temps, y_volts = np.array(x_temps), np.array(y_volts);
                y_normalized = y_volts / y0_ref if y0_ref != 0 else y_volts
                fit_x = np.linspace(min(x_temps), max(x_temps), 100)
                self.plot_widget.plot(x_temps, y_normalized, pen=None, symbol='o', symbolBrush=color)
                self.plot_widget.plot(fit_x, poly(fit_x), pen=pg.mkPen(color, width=2), name=f'CH{i + 1} 拟合')

    def _on_save_plot_image(self):
        """Saves the current plot as a PNG image."""
        current_time = QDateTime.currentDateTime().toString("yyyy-MM-dd_hh-mm-ss")
        default_filename = f"temperature_plot_{current_time}"
        filename, selected_filter = QFileDialog.getSaveFileName(
            self, "保存图像", default_filename, "PNG Image (*.png)"
        )
        if not filename: return
        try:
            if not filename.lower().endswith('.png'): filename += '.png'
            exporter = pg.exporters.ImageExporter(self.plot_widget.plotItem)
            exporter.export(filename)
            QMessageBox.information(self, "成功", f"图像已成功保存到:\n{filename}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存图像失败: {e}")

    @pyqtSlot(list)
    def on_voltages_received(self, filtered_voltages: list):
        if self.is_waiting_for_sample and len(filtered_voltages) == 12:
            self.sample_timeout_timer.stop();
            temp = self.pending_calibration_info.get('temp');
            point = {}
            if self.radio_single.isChecked():
                ch_index = self.pending_calibration_info.get('channel');
                voltage = filtered_voltages[ch_index];
                point = {'temp': temp, 'voltage': voltage, 'channel': ch_index}
            else:
                point = {'temp': temp, 'voltages': filtered_voltages}
            if self.data_model.add_point(point):
                self._add_point_to_table(point); self._sort_and_refresh_table()  # Refresh after adding
            else:
                QMessageBox.warning(self, "添加失败", f"温度为 {temp}°C 的数据点已存在，无法重复添加。")
            self._reset_sample_state()

    def _on_sample_clicked(self):
        if self.is_waiting_for_sample: return
        self.pending_calibration_info = {'temp': self.temp_spinbox.value(), 'channel': self.channel_combobox.currentIndex()}
        self.is_waiting_for_sample = True;
        self.sample_button.setVisible(False);
        self.cancel_button.setVisible(True)
        self.sample_timeout_timer.start(3000);
        self.request_pd_voltage_reading.emit()

    def _on_cancel_clicked(self):
        self.sample_timeout_timer.stop(); self._reset_sample_state()

    def _on_sample_timeout(self):
        QMessageBox.warning(self, "采样超时", "未能在3秒内收到设备响应。"); self._reset_sample_state()

    def _reset_sample_state(self):
        self.is_waiting_for_sample = False; self.sample_button.setVisible(True); self.cancel_button.setVisible(False)

    def _on_select_reference(self):
        selected_rows = self.table_widget.selectionModel().selectedRows()
        if not selected_rows: QMessageBox.warning(self, "警告", "请先选择一个数据点。"); return
        row = selected_rows[0].row();
        self.reference_point = self.data_model.data[row]
        base_text = f"T={self.reference_point['temp']:.2f}°C"
        if self.radio_single.isChecked():
            channel_index = self.reference_point.get('channel', 0);
            label_text = f"基准点: CH{channel_index + 1}, {base_text}"
        else:
            label_text = f"基准点: {base_text}"
        self.ref_label.setText(label_text);
        QMessageBox.information(self, "提示", "基准点已设置。")

    def closeEvent(self, event):
        self.hide(); self.window_closed.emit(); event.ignore()

    @pyqtSlot(bool)
    def on_connection_status_changed(self, is_connected):
        self.control_groupbox.setEnabled(is_connected)
        if not is_connected and self.is_waiting_for_sample: self.sample_timeout_timer.stop(); self._reset_sample_state(); QMessageBox.warning(self, "连接断开",
                                                                                                                                              "串口连接已断开，采样已取消。")

    @pyqtSlot(QTableWidgetItem)
    def _on_table_item_changed(self, item: QTableWidgetItem):
        row, col = item.row(), item.column();
        is_single = self.radio_single.isChecked()
        if is_single and col == 1:
            try:
                new_channel_index = int(item.text().split(' ')[1]) - 1;
                current_temp = self.data_model.data[row]['temp']
                if self.data_model._is_duplicate(current_temp, new_channel_index, is_single, ignore_row=row):
                    QMessageBox.warning(self, "修改失败", "修改后的(温度, 通道)组合与其他数据点重复，操作已取消。")
                    old_channel_index = self.data_model.data[row].get('channel', 0)
                    self.table_widget.blockSignals(True);
                    item.setText(f"通道 {old_channel_index + 1}");
                    self.table_widget.blockSignals(False)
                else:
                    self.data_model.data[row]['channel'] = new_channel_index
            except (ValueError, IndexError):
                pass
            return
        old_point = self.data_model.data[row];
        old_value_str = ""
        if col == 0:
            old_value_str = f"{old_point['temp']:.2f}"
        elif is_single and col == 2:
            old_value_str = f"{old_point['voltage']:.4f}"
        elif not is_single and col > 0:
            old_value_str = f"{old_point['voltages'][col - 1]:.4f}"
        try:
            new_value = float(item.text())
        except (ValueError, TypeError):
            self.table_widget.blockSignals(True); item.setText(old_value_str); self.table_widget.blockSignals(False); return
        if not self.data_model.update_value(row, col, new_value, is_single):
            QMessageBox.warning(self, "修改失败", "修改后的温度与其他数据点重复，操作已取消。")
            self.table_widget.blockSignals(True);
            item.setText(old_value_str);
            self.table_widget.blockSignals(False)
        else:
            print(f"数据模型已更新: 第{row + 1}行, {self.data_model.data[row]}")

    def _on_add_row(self):
        is_single = self.radio_single.isChecked();
        ch_index = self.channel_combobox.currentIndex()
        if is_single:
            new_point = {'temp': 99.0, 'voltage': 0.0, 'channel': ch_index}
        else:
            new_point = {'temp': 99.0, 'voltages': [0.0] * 12}
        if self.data_model.add_point(new_point):
            self._add_point_to_table(new_point);
            self._sort_and_refresh_table();
            self.table_widget.editItem(self.table_widget.item(self.table_widget.rowCount() - 1, 0))
        else:
            channel_str = f"通道 {ch_index + 1}" if is_single else "所有通道"
            QMessageBox.warning(self, "添加失败", f"无法添加新行，因为在 {channel_str} 中已存在一个温度为 {new_point['temp']}°C 的数据点。\n请先修改现有数据点的温度。")

    def _on_delete_row(self):
        selected_rows = self.table_widget.selectionModel().selectedRows()
        if not selected_rows: QMessageBox.warning(self, "警告", "请先在表格中选择要删除的行。"); return
        # IMPORTANT: Convert visual rows to model indices before deleting
        # Since our model and view are always in sync, the row indices are the same.
        rows_to_delete = sorted([index.row() for index in selected_rows], reverse=True)
        for row in rows_to_delete: self.data_model.remove_row(row)
        self._sort_and_refresh_table()  # Refresh after deleting

    def _on_save_results(self):
        if not self.fit_polynomials: QMessageBox.warning(self, "Warning", "No fitting data available to save."); return
        if not self.reference_point: QMessageBox.warning(self, "Warning", "Reference point is not set. Cannot save."); return
        if self.radio_single.isChecked():
            self._save_single_channel_json()
        else:
            self._save_12_channel_json()

    def _save_single_channel_json(self):
        """
        Saves a single channel's data, now including its specific reference voltage.
        """
        ch_index = self.channel_combobox.currentIndex()
        if ch_index not in self.fit_polynomials:
            QMessageBox.warning(self, "Error", f"No fit found for Channel {ch_index + 1}.")
            return

        physical_ch_num, ok = QInputDialog.getInt(self, "输入物理通道号", "请输入此通道的物理编号:", ch_index + 1, 1, 100, 1)
        if not ok:
            return

        filename, _ = QFileDialog.getSaveFileName(self, "保存单路标定数据", f"calibration_ch{physical_ch_num}.json", "JSON Files (*.json)")
        if not filename:
            return

        poly = self.fit_polynomials[ch_index]

        fit_results = {
            str(physical_ch_num): {
                "fit_params": poly.coeffs.tolist(),
                "equation": self._generate_json_equation_string(poly)
            }
        }

        reference_voltages = {
            str(physical_ch_num): self.reference_point.get('voltage', 0.0)
        }

        output_data = {
            "unit": "C",
            "mode": "single-channel",
            "reference_temperature": self.reference_point['temp'],
            "reference_voltages": reference_voltages,  # Add to output
            "fit_method": "poly",
            "calibration_data(unit:C)": self._get_formatted_cal_data(is_single_mode=True, channel_index=ch_index),
            "fit_results": fit_results
        }

        try:
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(output_data, f, indent=2)
            QMessageBox.information(self, "成功", f"通道 {physical_ch_num} 的标定数据已保存到:\n{filename}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存文件失败: {e}")

    def _save_12_channel_json(self):
        """
        Saves all 12 channels' data, now including all reference voltages.
        """
        filename, _ = QFileDialog.getSaveFileName(self, "保存12路标定数据", "calibration_all_channels.json", "JSON Files (*.json)")
        if not filename:
            return
        reference_voltages = {}
        ref_volts_list = self.reference_point.get('voltages', [0.0] * 12)
        for i in range(12):
            reference_voltages[str(i + 1)] = ref_volts_list[i]
        output_data = {
            "unit": "C",
            "mode": "12-channel",
            "reference_temperature": self.reference_point['temp'],
            "reference_voltages": reference_voltages,  # Add to output
            "fit_method": "poly",
            "calibration_data(unit:C)": self._get_formatted_cal_data(is_single_mode=False, channel_index=0, all_channels=True),
            "fit_results": {}
        }
        fit_results = {}
        for i in range(12):
            if i in self.fit_polynomials:
                poly = self.fit_polynomials[i]
                channel_fit = {
                    "fit_params": poly.coeffs.tolist(),
                    "equation": self._generate_json_equation_string(poly)
                }
                fit_results[str(i + 1)] = channel_fit

        output_data["fit_results"] = fit_results
        try:
            with open(filename, 'w') as f:
                json.dump(output_data, f, indent=2)
            QMessageBox.information(self, "成功", f"所有12通道的标定数据已保存到:\n{filename}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存文件失败: {e}")

    def _get_formatted_cal_data(self, is_single_mode: bool, channel_index: int, all_channels: bool = False) -> dict:
        if all_channels and not is_single_mode:
            grouped_data = {}
            for point in self.data_model.data:
                temp_key = f"{point['temp']:.1f}";
                channel_voltage_map = {str(i + 1): voltage for i, voltage in enumerate(point.get('voltages', []))};
                grouped_data[temp_key] = channel_voltage_map
            return grouped_data
        if is_single_mode:
            grouped_data = defaultdict(dict)
            for point in self.data_model.data:
                if point.get('channel') == channel_index:
                    temp_key = f"{point['temp']:.1f}";
                    ch_key = str(point.get('channel', 0) + 1);
                    grouped_data[temp_key][ch_key] = point.get('voltage')
            return dict(grouped_data)
        return {}

    def _generate_json_equation_string(self, poly: np.poly1d) -> str:
        terms = []
        for i, coeff in enumerate(poly.coeffs):
            power = poly.order - i;
            term = f"{coeff:.3e}x^{power}";
            terms.append(term)
        return "y = " + " + ".join(terms)